/*
 * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.naming.ldap;

import java.util.List;
import java.util.ArrayList;

import javax.naming.InvalidNameException;

/*
 * RFC2253Parser implements a recursive descent parser for a single DN.
 */
final class Rfc2253Parser {

  private final String name;      // DN being parsed
  private final char[] chars;     // characters in LDAP name being parsed
  private final int len;  // length of "chars"
  private int cur = 0;    // index of first unconsumed char in "chars"

  /*
   * Given an LDAP DN in string form, returns a parser for it.
   */
  Rfc2253Parser(String name) {
    this.name = name;
    len = name.length();
    chars = name.toCharArray();
  }

        /*
         * Parses the DN, returning a List of its RDNs.
         */
  // public List<Rdn> getDN() throws InvalidNameException {

  List<Rdn> parseDn() throws InvalidNameException {
    cur = 0;

    // ArrayList<Rdn> rdns =
    //  new ArrayList<Rdn>(len / 3 + 10);  // leave room for growth

    ArrayList<Rdn> rdns =
        new ArrayList<>(len / 3 + 10);  // leave room for growth

    if (len == 0) {
      return rdns;
    }

    rdns.add(doParse(new Rdn()));
    while (cur < len) {
      if (chars[cur] == ',' || chars[cur] == ';') {
        ++cur;
        rdns.add(0, doParse(new Rdn()));
      } else {
        throw new InvalidNameException("Invalid name: " + name);
      }
    }
    return rdns;
  }

  /*
   * Parses the DN, if it is known to contain a single RDN.
   */
  Rdn parseRdn() throws InvalidNameException {
    return parseRdn(new Rdn());
  }

  /*
   * Parses the DN, if it is known to contain a single RDN.
   */
  Rdn parseRdn(Rdn rdn) throws InvalidNameException {
    rdn = doParse(rdn);
    if (cur < len) {
      throw new InvalidNameException("Invalid RDN: " + name);
    }
    return rdn;
  }

  /*
   * Parses the next RDN and returns it.  Throws an exception if
   * none is found.  Leading and trailing whitespace is consumed.
   */
  private Rdn doParse(Rdn rdn) throws InvalidNameException {

    while (cur < len) {
      consumeWhitespace();
      String attrType = parseAttrType();
      consumeWhitespace();
      if (cur >= len || chars[cur] != '=') {
        throw new InvalidNameException("Invalid name: " + name);
      }
      ++cur;          // consume '='
      consumeWhitespace();
      String value = parseAttrValue();
      consumeWhitespace();

      rdn.put(attrType, Rdn.unescapeValue(value));
      if (cur >= len || chars[cur] != '+') {
        break;
      }
      ++cur;          // consume '+'
    }
    rdn.sort();
    return rdn;
  }

  /*
   * Returns the attribute type that begins at the next unconsumed
   * char.  No leading whitespace is expected.
   * This routine is more generous than RFC 2253.  It accepts
   * attribute types composed of any nonempty combination of Unicode
   * letters, Unicode digits, '.', '-', and internal space characters.
   */
  private String parseAttrType() throws InvalidNameException {

    final int beg = cur;
    while (cur < len) {
      char c = chars[cur];
      if (Character.isLetterOrDigit(c) ||
          c == '.' ||
          c == '-' ||
          c == ' ') {
        ++cur;
      } else {
        break;
      }
    }
    // Back out any trailing spaces.
    while ((cur > beg) && (chars[cur - 1] == ' ')) {
      --cur;
    }

    if (beg == cur) {
      throw new InvalidNameException("Invalid name: " + name);
    }
    return new String(chars, beg, cur - beg);
  }

  /*
   * Returns the attribute value that begins at the next unconsumed
   * char.  No leading whitespace is expected.
   */
  private String parseAttrValue() throws InvalidNameException {

    if (cur < len && chars[cur] == '#') {
      return parseBinaryAttrValue();
    } else if (cur < len && chars[cur] == '"') {
      return parseQuotedAttrValue();
    } else {
      return parseStringAttrValue();
    }
  }

  private String parseBinaryAttrValue() throws InvalidNameException {
    final int beg = cur;
    ++cur;                      // consume '#'
    while ((cur < len) &&
        Character.isLetterOrDigit(chars[cur])) {
      ++cur;
    }
    return new String(chars, beg, cur - beg);
  }

  private String parseQuotedAttrValue() throws InvalidNameException {

    final int beg = cur;
    ++cur;                      // consume '"'

    while ((cur < len) && chars[cur] != '"') {
      if (chars[cur] == '\\') {
        ++cur;              // consume backslash, then what follows
      }
      ++cur;
    }
    if (cur >= len) {   // no closing quote
      throw new InvalidNameException("Invalid name: " + name);
    }
    ++cur;      // consume closing quote

    return new String(chars, beg, cur - beg);
  }

  private String parseStringAttrValue() throws InvalidNameException {

    final int beg = cur;
    int esc = -1;       // index of the most recently escaped character

    while ((cur < len) && !atTerminator()) {
      if (chars[cur] == '\\') {
        ++cur;              // consume backslash, then what follows
        esc = cur;
      }
      ++cur;
    }
    if (cur > len) {            // 'twas backslash followed by nothing
      throw new InvalidNameException("Invalid name: " + name);
    }

    // Trim off (unescaped) trailing whitespace.
    int end;
    for (end = cur; end > beg; end--) {
      if (!isWhitespace(chars[end - 1]) || (esc == end - 1)) {
        break;
      }
    }
    return new String(chars, beg, end - beg);
  }

  private void consumeWhitespace() {
    while ((cur < len) && isWhitespace(chars[cur])) {
      ++cur;
    }
  }

  /*
   * Returns true if next unconsumed character is one that terminates
   * a string attribute value.
   */
  private boolean atTerminator() {
    return (cur < len &&
        (chars[cur] == ',' ||
            chars[cur] == ';' ||
            chars[cur] == '+'));
  }

  /*
   * Best guess as to what RFC 2253 means by "whitespace".
   */
  private static boolean isWhitespace(char c) {
    return (c == ' ' || c == '\r');
  }
}
